חקור חלופות TypeScript חזקות ל-enums: const assertions ו-union types. למד מתי להשתמש בכל אחת לקוד יציב וקל לתחזוקה.
מעבר ל-Enums: TypeScript Const Assertions מול Union Types
בעולם ה-JavaScript בעל הטיפוסים הסטטיים עם TypeScript, enums היו זמן רב הבחירה המועדפת לייצוג קבוצה קבועה של קבועים בעלי שמות. הם מציעים דרך ברורה וקריאה להגדרת אוסף של ערכים קשורים. עם זאת, ככל שפרויקטים גדלים ומתפתחים, מפתחים מחפשים לעיתים קרובות חלופות גמישות יותר ולעיתים ביצועיות יותר. שתי מתחרות חזקות שעולות לעיתים קרובות הן const assertions ו-union types. פוסט זה מתעמק בניואנסים של שימוש בחלופות אלו ל-enums מסורתיים, מספק דוגמאות מעשיות ומנחה אותך מתי לבחור באיזו מהן.
הבנת Enums מסורתיים ב-TypeScript
לפני שנחקור את החלופות, חיוני להבין היטב כיצד פועלים enums סטנדרטיים ב-TypeScript. Enums מאפשרים לך להגדיר קבוצה של קבועים מספריים או מחרוזתיים בעלי שמות. הם יכולים להיות מספריים (ברירת המחדל) או מבוססי מחרוזת.
Enums מספריים
כברירת מחדל, לחברי enum מוקצים ערכים מספריים החל מ-0.
enum DirectionNumeric {
Up,
Down,
Left,
Right
}
let myDirection: DirectionNumeric = DirectionNumeric.Up;
console.log(myDirection); // Output: 0
ניתן גם להקצות ערכים מספריים במפורש.
enum StatusCode {
Success = 200,
NotFound = 404,
InternalError = 500
}
let responseStatus: StatusCode = StatusCode.Success;
console.log(responseStatus); // Output: 200
Enums מחרוזתיים
enums מחרוזתיים מועדפים לעיתים קרובות בשל חווית דיבוג משופרת, שכן שמות החברים נשמרים ב-JavaScript המקומפל.
enum ColorString {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
let favoriteColor: ColorString = ColorString.Blue;
console.log(favoriteColor); // Output: "BLUE"
התקורה של Enums
בעוד ש-enums נוחים, הם מגיעים עם תקורה קלה. כאשר מקומפלים ל-JavaScript, enums של TypeScript הופכים לאובייקטים שלעיתים קרובות יש להם מיפויים הפוכים (לדוגמה, מיפוי הערך המספרי בחזרה לשם ה-enum). זה יכול להיות שימושי אך גם תורם לגודל החבילה וייתכן שלא תמיד יהיה נחוץ.
שקול את enum המחרוזת הפשוט הזה:
enum Status {
Pending = "PENDING",
Processing = "PROCESSING",
Completed = "COMPLETED"
}
ב-JavaScript, זה עשוי להפוך למשהו כזה:
var Status;
(function (Status) {
Status[\"Pending\"] = \"PENDING\";
Status[\"Processing\"] = \"PROCESSING\";
Status[\"Completed\"] = \"COMPLETED\";
})(Status || (Status = {}));
עבור קבוצות פשוטות ולקריאה בלבד של קבועים, קוד זה שנוצר עשוי להרגיש קצת מוגזם.
חלופה 1: Const Assertions
Const assertions הן תכונת TypeScript עוצמתית המאפשרת לך לומר לקומפיילר להסיק את הטיפוס הספציפי ביותר האפשרי עבור ערך. כאשר משתמשים בהן עם מערכים או אובייקטים המיועדים לייצג קבוצה קבועה של ערכים, הן יכולות לשמש כחלופה קלת משקל ל-enums.
Const Assertions עם מערכים
ניתן ליצור מערך של ליטרלים מחרוזתיים ולאחר מכן להשתמש ב-assertion מסוג const כדי להפוך את הטיפוס שלו לבלתי ניתן לשינוי ואת האלמנטים שלו לטיפוסים ליטרליים.
const statusArray = ["PENDING", "PROCESSING", "COMPLETED"] as const;
type StatusType = typeof statusArray[number];
let currentStatus: StatusType = "PROCESSING";
// currentStatus = "FAILED"; // Error: Type '"FAILED"' is not assignable to type 'StatusType'.
function processStatus(status: StatusType) {
console.log(`Processing status: ${status}`);
}
processStatus("COMPLETED");
בואו נפרק מה קורה כאן:
as const: assertion זו אומרת ל-TypeScript להתייחס למערך כאל קריאה בלבד ולהסיק את הטיפוסים הליטרליים הספציפיים ביותר עבור האלמנטים שלו. לכן, במקום `string[]`, הטיפוס הופך ל-`readonly ["PENDING", "PROCESSING", "COMPLETED"]`.typeof statusArray[number]: זהו טיפוס ממופה. הוא עובר על כל האינדקסים של ה-statusArrayומחלץ את הטיפוסים הליטרליים שלהם. חתימת האינדקסnumberבעצם אומרת "תן לי את הטיפוס של כל אלמנט במערך זה." התוצאה היא union type:"PENDING" | "PROCESSING" | "COMPLETED".
גישה זו מספקת בטיחות טיפוסים בדומה ל-enums מחרוזתיים אך מייצרת JavaScript מינימלי. ה-statusArray עצמו נשאר מערך של מחרוזות ב-JavaScript.
Const Assertions עם אובייקטים
Const assertions חזקות אף יותר כאשר הן מיושמות על אובייקטים. ניתן להגדיר אובייקט שבו המפתחות מייצגים את הקבועים בעלי השמות שלך והערכים הם המחרוזות או המספרים הליטרליים.
const userRoles = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER"
} as const;
type UserRole = typeof userRoles[keyof typeof userRoles];
let currentUserRole: UserRole = "EDITOR";
// currentUserRole = "GUEST"; // Error: Type '"GUEST"' is not assignable to type 'UserRole'.
function displayRole(role: UserRole) {
console.log(`User role is: ${role}`);
}
displayRole(userRoles.Admin); // Valid
displayRole("EDITOR"); // Valid
בדוגמה זו של אובייקט:
as const: assertion זו הופכת את האובייקט כולו לקריאה בלבד. חשוב מכך, היא מסיקה טיפוסים ליטרליים עבור כל ערכי המאפיינים (לדוגמה,"ADMIN"במקוםstring) והופכת את המאפיינים עצמם לקריאה בלבד.keyof typeof userRoles: ביטוי זה מביא ל-union של מפתחות אובייקט ה-userRoles, שהוא"Admin" | "Editor" | "Viewer".typeof userRoles[keyof typeof userRoles]: זהו lookup type. הוא לוקח את union המפתחות ומשתמש בו כדי לחפש את הערכים המתאימים בטיפוס ה-userRoles. זה מביא ל-union של הערכים:"ADMIN" | "EDITOR" | "VIEWER", שהוא הטיפוס הרצוי לנו עבור תפקידים.
פלט ה-JavaScript עבור userRoles יהיה אובייקט JavaScript רגיל:
var userRoles = {
Admin: "ADMIN",
Editor: "EDITOR",
Viewer: "VIEWER"
};
זה קל משמעותית מ-enum טיפוסי.
מתי להשתמש ב-Const Assertions
- קבועים לקריאה בלבד: כאשר אתה צריך קבוצה קבועה של ליטרלים מחרוזתיים או מספריים שאסור שישתנו בזמן ריצה.
- פלט JavaScript מינימלי: אם אתה מודאג מגודל החבילה ורוצה את הייצוג הביצועי ביותר בזמן ריצה עבור הקבועים שלך.
- מבנה דמוי אובייקט: כאשר אתה מעדיף את קריאות זוגות מפתח-ערך, בדומה לאופן שבו היית מבנה נתונים או תצורה.
- קבוצות מבוססות מחרוזת: שימושי במיוחד לייצוג מצבים, טיפוסים או קטגוריות המזוהים בצורה הטובה ביותר על ידי מחרוזות תיאוריות.
חלופה 2: Union Types
Union types מאפשרים לך להצהיר שמשתנה יכול להחזיק ערך מאחד מכמה טיפוסים. כאשר משלבים אותם עם literal types (ליטרלים של מחרוזת, מספר, בוליאני), הם מהווים דרך עוצמתית להגדיר קבוצה של ערכים מותרים מבלי צורך בהצהרת קבוע מפורשת עבור הקבוצה עצמה.
Union Types עם ליטרלים מחרוזתיים
ניתן להגדיר ישירות union של ליטרלים מחרוזתיים.
type TrafficLightColor = "RED" | "YELLOW" | "GREEN";
let currentLight: TrafficLightColor = "YELLOW";
// currentLight = "BLUE"; // Error: Type '"BLUE"' is not assignable to type 'TrafficLightColor'.
function changeLight(color: TrafficLightColor) {
console.log(`Changing light to: ${color}`);
}
changeLight("RED");
// changeLight("REDDY"); // Error
Union Types עם ליטרלים מספריים
באופן דומה, ניתן להשתמש בליטרלים מספריים.
type HttpStatusCode = 200 | 400 | 404 | 500;
let responseCode: HttpStatusCode = 404;
// responseCode = 201; // Error: Type '201' is not assignable to type 'HttpStatusCode'.
function handleResponse(code: HttpStatusCode) {
if (code === 200) {
console.log("Success!");
} else {
console.log(`Error code: ${code}`);
}
}
handleResponse(500);
מתי להשתמש ב-Union Types
- קבוצות פשוטות וישירות: כאשר קבוצת הערכים המותרים קטנה, ברורה ואינה דורשת מפתחות תיאוריים מעבר לערכים עצמם.
- קבועים מרומזים: כאשר אינך צריך להתייחס לקבוע בעל שם עבור הקבוצה עצמה, אלא להשתמש ישירות בערכים הליטרליים.
- תמציתיות מקסימלית: לתרחישים פשוטים שבהם הגדרת אובייקט או מערך ייעודי מרגישה כמו הגזמה.
- פרמטרים/טיפוסי החזר של פונקציות: מצוין להגדרת הקבוצה המדויקת של קלט/פלט מחרוזת או מספר קבילים עבור פונקציות.
השוואת Enums, Const Assertions ו-Union Types
בואו נסכם את ההבדלים העיקריים ומקרי השימוש:
התנהגות בזמן ריצה
- Enums: מייצרים אובייקטי JavaScript, עם מיפויים הפוכים פוטנציאליים.
- Const Assertions (מערכים/אובייקטים): מייצרים מערכי JavaScript או אובייקטים פשוטים. מידע הטיפוס נמחק בזמן ריצה, אך מבנה הנתונים נשמר.
- Union Types (עם ליטרלים): אין ייצוג בזמן ריצה עבור ה-union עצמו. הערכים הם רק ליטרלים. בדיקת הטיפוסים מתרחשת אך ורק בזמן קומפילציה.
קריאות וביטוי
- Enums: קריאות גבוהה, במיוחד עם שמות תיאוריים. יכול להיות מפורט יותר.
- Const Assertions (אובייקטים): קריאות טובה באמצעות זוגות מפתח-ערך, המחקות תצורות או הגדרות.
- Const Assertions (מערכים): פחות קריא לייצוג קבועים בעלי שמות, יותר לרשימה מסודרת של ערכים בלבד.
- Union Types: תמציתי מאוד. הקריאות תלויה בבהירות הערכים הליטרליים עצמם.
בטיחות טיפוסים
- כל שלוש הגישות מציעות בטיחות טיפוסים חזקה. הן מבטיחות שרק ערכים תקפים ומוגדרים מראש ניתנים להקצאה למשתנים או להעברה לפונקציות.
גודל חבילה
- Enums: בדרך כלל הגדולים ביותר בשל אובייקטי JavaScript שנוצרו.
- Const Assertions: קטנים יותר מ-enums, שכן הם מייצרים מבני נתונים רגילים.
- Union Types: הקטנים ביותר, שכן הם אינם מייצרים מבנה נתונים ספציפי בזמן ריצה עבור הטיפוס עצמו, אלא מסתמכים רק על ערכים ליטרליים.
מטריצת מקרי שימוש
הנה מדריך מהיר:
| תכונה | TypeScript Enum | Const Assertion (אובייקט) | Const Assertion (מערך) | Union Type (ליטרלים) |
|---|---|---|---|---|
| פלט זמן ריצה | אובייקט JS (עם מיפוי הפוך) | אובייקט JS רגיל | מערך JS רגיל | אין (רק ערכים ליטרליים) |
| קריאות (קבועים בעלי שמות) | גבוהה | גבוהה | בינונית | נמוכה (ערכים הם שמות) |
| גודל חבילה | הגדול ביותר | בינוני | בינוני | הקטן ביותר |
| גמישות | טובה | טובה | טובה | מצוינת (עבור קבוצות פשוטות) |
| שימוש נפוץ | מצבים, קודי סטטוס, קטגוריות | תצורה, הגדרות תפקידים, דגלי תכונות | רשימות מסודרות של ערכים בלתי ניתנים לשינוי | פרמטרים לפונקציות, ערכים מוגבלים פשוטים |
דוגמאות מעשיות ושיטות עבודה מומלצות
דוגמה 1: ייצוג קודי סטטוס של API
Enum:
enum ApiStatus {
Success = "SUCCESS",
Error = "ERROR",
Pending = "PENDING"
}
function handleApiResponse(status: ApiStatus) {
// ... logic ...
}
Const Assertion (אובייקט):
const apiStatusCodes = {
SUCCESS: "SUCCESS",
ERROR: "ERROR",
PENDING: "PENDING"
} as const;
type ApiStatus = typeof apiStatusCodes[keyof typeof apiStatusCodes];
function handleApiResponse(status: ApiStatus) {
// ... logic ...
}
Union Type:
type ApiStatus = "SUCCESS" | "ERROR" | "PENDING";
function handleApiResponse(status: ApiStatus) {
// ... logic ...
}
המלצה: עבור תרחיש זה, union type הוא לרוב התמציתי והיעיל ביותר. הערכים הליטרליים עצמם תיאוריים מספיק. אם הייתם צריכים לשייך מטא-נתונים נוספים לכל סטטוס (לדוגמה, הודעה ידידותית למשתמש), אובייקט const assertion היה בחירה טובה יותר.
דוגמה 2: הגדרת תפקידי משתמשים
Enum:
enum UserRoleEnum {
Admin = "ADMIN",
Moderator = "MODERATOR",
User = "USER"
}
function getUserPermissions(role: UserRoleEnum) {
// ... logic ...
}
Const Assertion (אובייקט):
const userRolesObject = {
Admin: "ADMIN",
Moderator: "MODERATOR",
User: "USER"
} as const;
type UserRole = typeof userRolesObject[keyof typeof userRolesObject];
function getUserPermissions(role: UserRole) {
// ... logic ...
}
Union Type:
type UserRole = "ADMIN" | "MODERATOR" | "USER";
function getUserPermissions(role: UserRole) {
// ... logic ...
}
המלצה: אובייקט const assertion משיג כאן איזון טוב. הוא מספק זוגות מפתח-ערך ברורים (לדוגמה, userRolesObject.Admin) שיכולים לשפר את הקריאות בעת התייחסות לתפקידים, תוך שמירה על ביצועים. union type הוא גם מתמודד חזק מאוד אם ליטרלים מחרוזתיים ישירים מספיקים.
דוגמה 3: ייצוג אפשרויות תצורה
דמיינו אובייקט תצורה עבור יישום גלובלי שעשוי לכלול ערכות נושא שונות.
Enum:
enum Theme {
Light = "light",
Dark = "dark",
System = "system"
}
interface AppConfig {
theme: Theme;
// ... other config options ...
}
Const Assertion (אובייקט):
const themes = {
Light: "light",
Dark: "dark",
System: "system"
} as const;
type Theme = typeof themes[keyof typeof themes];
interface AppConfig {
theme: Theme;
// ... other config options ...
}
Union Type:
type Theme = "light" | "dark" | "system";
interface AppConfig {
theme: Theme;
// ... other config options ...
}
המלצה: עבור הגדרות תצורה כמו ערכות נושא, אובייקט const assertion הוא לרוב אידיאלי. הוא מגדיר בבירור את האפשרויות הזמינות ואת ערכי המחרוזת המתאימים להן. המפתחות (Light, Dark, System) תיאוריים וממפים ישירות לערכים, מה שהופך את קוד התצורה למובן מאוד.
בחירת הכלי הנכון למשימה
ההחלטה בין enums של TypeScript, const assertions ו-union types אינה תמיד חד משמעית. לעיתים קרובות היא מסתכמת באיזון בין ביצועי זמן ריצה, גודל החבילה וקריאות/אקספרסיביות הקוד.
- בחר ב-Union Types כאשר אתה צריך קבוצה פשוטה ומוגבלת של ליטרלים מחרוזתיים או מספריים ורצוי תמציתיות מקסימלית. הם מצוינים עבור חתימות פונקציות והגבלות ערכים בסיסיות.
- בחר ב-Const Assertions (עם אובייקטים) כאשר אתה רוצה דרך מובנית וקריאה יותר להגדרת קבועים בעלי שמות, בדומה ל-enum, אך עם תקורה נמוכה משמעותית בזמן ריצה. זה נהדר עבור תצורה, תפקידים, או כל קבוצה שבה המפתחות מוסיפים משמעות רבה.
- בחר ב-Const Assertions (עם מערכים) כאשר אתה פשוט צריך רשימה מסודרת ובלתי ניתנת לשינוי של ערכים, והגישה הישירה באמצעות אינדקס חשובה יותר ממפתחות בעלי שמות.
- שקול TypeScript Enums כאשר אתה צריך את התכונות הספציפיות שלהם, כגון מיפוי הפוך (אם כי זה פחות נפוץ בפיתוח מודרני) או אם לצוות שלך יש העדפה חזקה והשפעת הביצועים זניחה עבור הפרויקט שלך.
בפרויקטים רבים של TypeScript מודרניים, תמצא נטייה ל-const assertions ול-union types על פני enums מסורתיים, במיוחד עבור קבועים מבוססי מחרוזת, בשל מאפייני הביצועים הטובים יותר שלהם ולעיתים קרובות פלט JavaScript פשוט יותר.
שיקולים גלובליים
בעת פיתוח יישומים לקהל גלובלי, הגדרות קבועים עקביות וצפויות הן קריטיות. הבחירות שדנו בהן (enums, const assertions, union types) כולן תורמות לעקביות זו על ידי אכיפת בטיחות טיפוסים בסביבות שונות ובמיקומי מפתחים שונים.
- עקביות: ללא קשר לשיטה שנבחרה, המפתח הוא עקביות בתוך הפרויקט שלך. אם תחליט להשתמש באובייקטי const assertion לתפקידים, הקפד על דפוס זה בכל בסיס הקוד.
- בינאום (i18n): בעת הגדרת תוויות או הודעות שיבואמו, השתמש במבנים בטוחים בטיפוסים אלה כדי להבטיח שנעשה שימוש רק במפתחות או מזהים תקפים. המחרוזות המתורגמות בפועל ינוהלו בנפרד באמצעות ספריות i18n. לדוגמה, אם יש לך שדה `status` שיכול להיות "PENDING", "PROCESSING", "COMPLETED", ספריית ה-i18n שלך תמפה מזהים פנימיים אלה לטקסט תצוגה מקומי.
- אזורי זמן ומטבעות: אמנם לא קשור ישירות ל-enums, זכור כי בעת טיפול בערכים כמו תאריכים, זמנים או מטבעות, מערכת הטיפוסים של TypeScript יכולה לעזור לאכוף שימוש נכון, אך ספריות חיצוניות נחוצות בדרך כלל לטיפול גלובלי מדויק. לדוגמה, union type של `Currency` יכול להיות מוגדר כ-"USD" | "EUR" | "GBP", אך לוגיקת ההמרה בפועל דורשת כלים מיוחדים.
מסקנה
TypeScript מספקת מגוון עשיר של כלים לניהול קבועים. בעוד ש-enums שירתו אותנו היטב, const assertions ו-union types מציעים חלופות משכנעות, ולעיתים קרובות בעלות ביצועים טובים יותר. על ידי הבנת ההבדלים ביניהם ובחירת הגישה הנכונה בהתבסס על הצרכים הספציפיים שלך—בין אם זה ביצועים, קריאות או תמציתיות—תוכל לכתוב קוד TypeScript חזק יותר, קל יותר לתחזוקה ויעיל יותר, שמתרחב גלובלית.
אימוץ חלופות אלה יכול להוביל לגודל חבילה קטן יותר, יישומים מהירים יותר וחווית מפתח צפויה יותר עבור הצוות הבינלאומי שלך.